/* ending.cpp
Copyright (c) 2008 Michael Zahniser
Please note the license terms (MIT license) at the end of this document.

This program reports or modifies the line endings of the input files.
See PrintHelp() below for a summary of the command line options.
*/



#include <iostream>
#include <fstream>
#include <string>
#include <cstring>

using std::cout;
using std::cin;
using std::endl;
using std::ifstream;
using std::ofstream;
using std::ostream;
using std::ios;
using std::string;
using std::memcpy;



// PrintHelp:
// Display a help message describing what the program does and how to use it.
void PrintHelp()
{
	cout << endl;
	cout << "ending [line ending conversion tool]" << endl;
	cout << "This program reports or modifies the line endings of the input files. Options:" << endl;
	cout << "-i: Inspect (report which type(s) of endings are used)" << endl;
	cout << "-x: convert to Unix (\\n line ending; this is the default option)" << endl;
	cout << "-w: convert to Windows (\\r\\n line ending)" << endl;
	cout << "-m: convert to Old Mac (\\r)." << endl;
	cout << "-h: Print this help message." << endl;
	cout << endl;
}



// class LineCounts:
// Stores the numer of line endings of each type that were found in the input.
class LineCounts {
public:
	LineCounts(const char *it, const char *end);
	
	int UnixCount() const { return unixCount; }
	int WindowsCount() const { return windowsCount; }
	int MacCount() const { return macCount; }
	
	// Return true if only endings of the given type were found.
	bool Matches(const string &ending) const;
	
	friend ostream &operator<<(ostream &out, const LineCounts &counts);
	
	
private:
	int unixCount;
	int windowsCount;
	int macCount;
};



// Process a given file, converting its line endings to the given string.
// If the string is empty, report the current line endings without
// modifying the file.
void ProcessFile(const char *path, const string &ending);
// Read text from standard input, then process it.
void ProcessInput(const string &ending);
// Given iterators to an in-memory copy of a file, write it out,
// replacing all line endings with the given ending.
void Write(const char *it, const char *end, ostream &out, const string &ending);



// main:
int main(int argc, char *argv[])
{
	// Parse the command line arguments for options.
	string ending = "\n";
	for(char **arg = argv + 1; *arg; ++arg)
		if(**arg == '-')
		{
			char option = (*arg)[1];
			if(option == 'i')
				ending = "";
			else if(option == 'x')
				ending = "\n";
			else if(option == 'w')
				ending = "\r\n";
			else if(option == 'm')
				ending = "\r";
			else
			{
				PrintHelp();
				return 0;
			}
		}
	
	// Everything that isn't an option (-*) is a file to process.
	int filesProcessed = 0;
	for(char **arg = argv + 1; *arg; ++arg)
		if(**arg != '-')
		{
			ProcessFile(*arg, ending);
			++filesProcessed;
		}
	
	// If no files were specified, process the standard input instead.
	if(!filesProcessed)
		ProcessInput(ending);
	
	return 0;
}



// ProcessFile:
// Process a given file, converting its line endings to the given string.
// If the string is empty, report the current line endings without
// modifying the file.
void ProcessFile(const char *path, const string &ending)
{
	// Optimization goal:
	// The code should be as fast as possible for the case when no
	// modifications need to be made to the file. This implies that
	// we should check the line endings first before trying to
	// reformat the file at all.
	
	// Load the entire file into a buffer.
	ifstream in(path, ios::in | ios::binary);
	in.seekg(0, ios::end);
	size_t length = in.tellg();
	in.seekg(0, ios::beg);
	
	char *buffer = new char[length];
	in.read(buffer, length);
	
	// Count line ending types.
	LineCounts counts(buffer, buffer + length);
	
	// If we're in inspection mode, print the results and return.
	if(ending.empty())
		cout << endl << path << endl << counts << endl;
	// Check to see if we have any work to do.
	else if(!counts.Matches(ending))
	{
		// Write the file, modifying the line endings.
		ofstream out(path, ios::out | ios::binary);
		Write(buffer, buffer + length, out, ending);
	}
	
	delete [] buffer;
}



// ProcessInput:
// Read text from standard input, then process it.
void ProcessInput(const string &ending)
{
	size_t reserved = 1024;
	size_t length = 0;
	char *buffer = new char[reserved];
	
	// Read text from cin into a memory buffer, resizing the buffer
	// as needed to fit all the input.
	while(true)
	{
		cin.read(buffer + length, reserved - length);
		length += cin.gcount();
		
		if(!cin)
			break;
		
		reserved *= 2;
		
		char *newBuffer = new char[reserved];
		memcpy(newBuffer, buffer, length);
		delete [] buffer;
		buffer = newBuffer;
	}
	
	// Count line ending types.
	LineCounts counts(buffer, buffer + length);
	
	// If we're in inspection mode, print the results and return.
	// Otherwise print the input, modifying the line endings.
	if(ending.empty())
		cout << endl << counts << endl;
	else
		Write(buffer, buffer + length, cout, ending);
	
	delete [] buffer;
}



// Write:
// Given iterators to an in-memory copy of a file, write it out,
// replacing all line endings with the given ending.
void Write(const char *it, const char *end, ostream &out, const string &ending)
{   
	while(it != end)
	{
		const char *begin = it;
		// Find the next line ending.
		for( ; it != end; ++it)
			if((*it == '\r') | (*it == '\n'))
				break;
		
		// Write everything up to but not including the ending.
		out.write(begin, it - begin);
		// Write the line ending. This has the side effect of ensuring that
		// every file ends in a newline (which g++ likes).
		out << ending;
		
		if(it == end)
			break;
		
		char last = *it;
		
		// Skip the line ending.
		++it;
		
		// If the next character is the second half of a \r\n, skip it.
		if(last == '\r' && it != end && *it == '\n')
			++it;
	}
}



// LineCounts constructor:
LineCounts::LineCounts(const char *it, const char *end)
	: unixCount(0), windowsCount(0), macCount(0)
{
	char last = '\0';
	
	for( ; it != end; ++it)
	{
		char next = *it;
		if(next == '\r')
			++macCount;
		else if(next == '\n')
		{
			if(last == '\r')
			{
				--macCount;
				++windowsCount;
			}
			else
				++unixCount;
		}
		last = next;
	}
}



// LineCounts Matches:
// Return true if only endings of the given type were found.
bool LineCounts::Matches(const string &ending) const
{
	return ((ending == "\n") & !(windowsCount | macCount)) |
		((ending == "\r\n") & !(unixCount | macCount)) |
		((ending == "\r") & !(unixCount | windowsCount));
}




// Output operator for LineCounts:
ostream &operator<<(ostream &out, const LineCounts &counts)
{
	if(counts.unixCount)
		out << "Unix (\\n): " << counts.unixCount << " lines. ";
	if(counts.windowsCount)
		out << "Windows (\\r\\n): " << counts.windowsCount << " lines. ";
	if(counts.macCount)
		out << "Old Mac (\\r): " << counts.macCount << " lines. ";
	return out;
}



/* Copyright (c) 2008 Michael Zahniser
 
 Permission is hereby granted, free of charge, to any person
 obtaining a copy of this software and associated documentation
 files (the "Software"), to deal in the Software without
 restriction, including without limitation the rights to use,
 copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the
 Software is furnished to do so, subject to the following
 conditions:
 
 The above copyright notice and this permission notice shall be
 included in all copies or substantial portions of the Software.
 
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 OTHER DEALINGS IN THE SOFTWARE. */
